<?php
namespace Peas\Utils;

use Peas\Support\Exception;

/**
 * Peas Framework
 *
 * 唯一ID生成工具类，参照twitter的snowflake算法，39位时间+9位服务器ID+15位计数序列，
 * 计数信息存储在共享内存中，使用信号量加锁
 *
 * @author  Hongzan Zhang <zhanghongzan@163.com>
 * @version $Id$
 */
class IdUtils
{
    const shmopKey       = 22103;      // 共享内存key
    const semKey         = 22104;      // 信号量key
    const beginTimestamp = 1443974400; // 起始时间戳

    const bitsOfServerId = 9;  // 第二段服务端标记长度
    const bitsOfSequence = 15; // 第三段计数序列长度

    const shiftOfTimestamp = 24; // 第一段时间左移位长度（后两段长度之和）
    const shiftOfServerId  = 15; // 第二段左移位长度（第三段长度）

    const maxServerId = 511;   // 最大服务器ID
    const maxSequence = 32765; // 最大计数序列

    private static $_serverId = 0; // 服务器ID


    /**
     * 初始化，设置服务器ID
     *
     * @param int $serverId
     * @throws Exception
     */
    public static function init($serverId)
    {
        if ($serverId < 0 || $serverId > self::maxServerId) {
            throw new Exception('服务器ID必须在0～' . self::maxServerId . '之间');
        }
        self::$_serverId = $serverId;
    }

    /**
     * 生成ID
     *
     * @return int
     */
    public static function nextId()
    {
        list($timestamp, $sequence) = self::_getSequence();
        while ($sequence > self::maxSequence) {
            list($timestamp, $sequence) = self::_getSequence();
        }
        $nextId = ((sprintf('%.0f', $timestamp) - sprintf('%.0f', self::beginTimestamp)) << self::shiftOfTimestamp)
            | ( self::$_serverId << self::shiftOfServerId ) | $sequence;
        return $nextId;
    }


    /**
     * 获取计数
     *
     * @return array [当前时间戳, 计数]
     */
    private static function _getSequence() {
        $semId = sem_get(self::semKey); // 创建信号量
        sem_acquire($semId); // 占有信号量（加锁）

        $shmId = shmop_open(self::shmopKey, 'a', 0644, 0);
        $sequenceInfo = '';
        if ($shmId) {
            $sequenceInfo = shmop_read($shmId, 0, shmop_size($shmId));
            shmop_delete($shmId);
            shmop_close($shmId);
        }
        $timestamp = time();
        $sequence = 0;
        if (!empty($sequenceInfo)) {
            list($lastTime, $lastSeq) = explode('|', $sequenceInfo);
            if ($timestamp < $lastTime) {
                $timestamp = $lastTime;
            }
            if ($lastTime == $timestamp) {
                $sequence = $lastSeq + 1;
            }
        }
        $newInfo = $timestamp . '|' . $sequence;
        $shmId = shmop_open(self::shmopKey, 'c', 0644, strlen($newInfo));
        shmop_write($shmId, $newInfo, 0);
        shmop_close($shmId);

        sem_release($semId); // 释放信号量（解锁）
        return [$timestamp, $sequence];
    }
}
